BACnet IP 协议完全指南

从零开始理解楼宇自控的"通用语言" · ANSI/ASHRAE 135 · ISO 16484-5

1

什么是 BACnet 协议

从一个例子说起

想象一栋现代化办公大楼,里面有成百上千台设备:空调、灯具、电梯、消防报警器、门禁……它们由不同厂家生产,各自"说着不同的语言"。如果要温度高于 28°C 时自动开空调,就需要一种统一的沟通规则,让所有设备能听懂彼此。

→ BACnet 就是这个"统一语言"——楼宇自控领域的"英语"。

BACnet 是什么

BACnet(Building Automation and Control Networks,楼宇自动化和控制网络)是专为楼宇自控设计的开放标准通信协议。由 ASHRAE(美国采暖、制冷与空调工程师学会)制定,标准编号 ANSI/ASHRAE 135,同时也是 ISO 16484-5 国际标准。

"开放标准"意味着:任何人都可以免费使用,不受任何一家公司控制。不同厂家的设备只要遵循 BACnet 标准,就能直接互通,无需购买额外的协议转换器。

BACnet 解决了什么问题
问题BACnet 的解决方案
不同厂家设备无法互通统一数据模型和通信服务
系统集成成本高兼容设备即插即用
缺乏标准化管理统一描述、命名、告警
厂商锁定开放标准,自由选型
BACnet 的应用领域
HVAC 暖通空调
温度控制 · 湿度调节 · 新风系统
照明控制
灯光开关 · 调光 · 场景模式
安防系统
门禁 · 入侵报警 · 视频联动
消防系统
烟感报警 · 喷淋 · 排烟联动
能源管理
能耗监测 · 节能优化
2

什么是 BACnet IP

BACnet 的多种传输方式

BACnet 支持"走不同线路"传输数据。就像信件内容格式相同,但可以通过快递公司、邮局或专人投递。BACnet/IP 是通过普通计算机网络传输的方式,是目前最主流的选择。

传输方式线路类型速度特点
BACnet/IP以太网/网络线最快直接利用现有 IP 网络,本驱动采用
BACnet MSTPRS-485 串行线较慢老旧设备改造场景
BACnet PTPRS-232 串行线很慢点对点拨号(已少用)
UDP 协议——"发短信"式的通信

BACnet IP 使用 UDP(User Datagram Protocol)通信。与 TCP("打电话"——先拨号接通再通话)不同,UDP 像"发短信"——直接发出去,无需事先建立连接,速度更快、开销更低。

为什么选 UDP?
① 实时性要求高,需要低延迟
② 设备资源有限,TCP 管理开销太大
③ BACnet 自身已有确认重传机制

端口与通信方式

默认端口 47808(十六进制 0xBAC0,取 BACnet 前三个字母)。
单播:一对一,直接向目标设备发消息。
广播:一对多,向网络中所有设备喊话(用于发现设备)。

传输层UDP 无连接("发短信")
默认端口47808(0xBAC0)
发现机制Who-Is / I-Am 广播
通信方式单播(日常)+ 广播(发现)
BACnet IP 通信示意
graph LR DRV["BACnet 驱动
192.168.1.10
端口 47808"] <--"UDP 数据包
通过以太网"--> DEV["BACnet 设备
192.168.1.100
端口 47808"] style DRV fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style DEV fill:#e8f0fe,stroke:#007aff,color:#1c1c1e
3

BACnet 对象模型——设备数据的组织方式

核心概念:对象(Object)

把一台 BACnet 设备想象成一部智能手机。手机上有很多"App"(天气、音乐、设置),每个 App 展示不同的数据。在 BACnet 中,每个 "App" 就是一个对象(Object):
· 温度传感器这个"App" → Analog Input 对象
· 空调开关这个"App" → Binary Output 对象

每个对象有多个属性(Property)——就像 App 显示的各种信息(当前值、名称、单位等)。

一台 BACnet 设备的内部结构示例
graph TB DEV["BACnet 设备(空调控制器)"] DOBJ["Device 对象
设备身份信息"] AI1["Analog Input #1
一楼大堂温度
当前值: 25.6C"] AI2["Analog Input #2
二楼办公区温度
当前值: 26.3C"] BO1["Binary Output #1
空调机组启停
当前值: 运行"] AO1["Analog Output #1
新风阀门开度
当前值: 75%"] DEV --> DOBJ & AI1 & AI2 & BO1 & AO1 style DEV fill:#007aff,color:#fff style DOBJ fill:#5856d6,color:#fff style AI1 fill:#34c759,color:#fff style AI2 fill:#34c759,color:#fff style BO1 fill:#ff9500,color:#fff style AO1 fill:#ff9500,color:#fff
九种标准对象类型(分三组)

模拟量(Analog) = 连续变化的数值(如温度 23.5); 二元量(Binary) = 只有两个状态(开/关); 多态量(Multi-State) = 多个固定状态(如高/中/低三档)。

分组对象类型缩写方向通俗解释生活例子
模拟量Analog InputAI只读从传感器读取的连续数值温度计读数 25.6
Analog OutputAO读写控制外部设备的连续值阀门开度设为 75%
Analog ValueAV读写设备内部参数/变量温度设定点 24
二元量Binary InputBI只读从设备读取的开/关状态风机是否在运行
Binary OutputBO读写控制设备的开/关开启/关闭风机
Binary ValueBV读写内部的开/关标志手动/自动模式标志
多态量Multi-State InputMSI只读读取的多选一状态阀门: 全关/半开/全开
Multi-State OutputMSO读写控制的多选一状态风扇: 关/低/中/高
Multi-State ValueMSV读写内部的多选一参数模式: 制冷/制热/通风
Input vs Output vs Value
Input
输入
数据从外部"流入"设备
只能读取,不能修改
例:传感器数据天然是只读的
Output
输出
数据从设备"流出"控制外部
可以读取,也可以修改
例:控制指令,既能看状态也能改状态
Value
值
设备内部的参数/变量
可以读取,也可以修改
例:设定点、阈值等软件参数
对象常用属性
属性编号通俗解释
Present Value85当前值——最核心属性,点位数据就是它
Description28对象的文字说明(通常用作点位名称)
Units117物理单位(如摄氏度、百分比)
Min/Max Pres Value69/70允许的最小值/最大值(量程范围)
Status Flags111状态标志(报警/故障/被覆盖/停用)
Priority Array87写入控制的优先级队列(见第 6 节)
COV Increment22变化多少才触发通知(见第 5 节)
对象的寻址——如何精确定位一个数据点
graph LR A["设备 IP 地址
192.168.1.100:47808"] --> B["设备实例号
Device ID: 101"] B --> C["对象类型
analogInput"] C --> D["对象实例号
#1"] D --> E["属性
presentValue"] E --> F["结果: 25.6"] style A fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style F fill:#e8f8e8,stroke:#34c759,color:#1c1c1e
4

BACnet 服务——设备之间怎么"对话"

什么是"服务"

"服务"就是设备之间互相通信的指令类型。就像餐厅点餐:
· ReadProperty = "这道菜是什么食材?"(查询信息)
· WriteProperty = "把这道菜做成不辣的"(修改信息)
· Who-Is = 前台喊"XX 号桌的客人在吗?"(广播找人)
· SubscribeCOV = "菜好了叫我"(订阅通知)

BACnet 核心服务一览
类别服务通俗解释实际用途
数据
读取
ReadProperty"告诉我某个属性的值"读取某个温度传感器的当前温度
ReadPropertyMultiple"一次性告诉我多个值"一条请求同时读取 100 个传感器
数据
写入
WriteProperty"把这个属性设为指定值"将空调设定温度改为 24
设备
发现
Who-Is"网络中的设备,谁在?"广播查找网络中所有设备
I-Am"我在!我是设备 X"设备回复自己的身份信息
COV
订阅
SubscribeCOV"值变了请通知我"订阅温度变化通知
COVNotification"你订阅的值变了"设备主动推送变化后的值
Who-Is / I-Am 设备发现流程
sequenceDiagram participant DRV as 驱动 participant DA as 设备 A participant DB as 设备 B participant DC as 设备 C DRV->>DA: Who-Is 广播 谁是 BACnet 设备 DRV->>DB: Who-Is 广播 DRV->>DC: Who-Is 广播 DA-->>DRV: I-Am 设备101 IP 192.168.1.100 DB-->>DRV: I-Am 设备102 IP 192.168.1.101 DC-->>DRV: I-Am 设备103 IP 192.168.1.102
5

COV 订阅——"值变了主动告诉我"

方式一:定时轮询

驱动每隔 10 秒问设备:"温度是多少?"
设备答:"25.6"。
10 秒后再问:"温度是多少?"
设备答:"25.6"。
问题:大部分时候没变化,白白浪费带宽;且最多有 10 秒延迟。

方式二:COV 订阅(推荐)

驱动跟设备说:"帮我盯着温度,变化超过 0.5 就告诉我。"
设备答:"好的。"
温度从 25.6 变为 26.2(变化 0.6 大于 0.5):
设备主动推送:"温度变了,现在 26.2!"
优势:不浪费带宽,几乎零延迟。

COV 订阅完整流程
sequenceDiagram participant DRV as 驱动 participant DEV as BACnet 设备 DRV->>DEV: 1 SubscribeCOV 订阅 AnalogInput-1 的值变化 DEV-->>DRV: 确认 订阅成功 DEV->>DRV: 2 初始通知 当前值 25.6 Note over DEV: 温度小幅波动未超过 0.5 阈值 DEV->>DRV: 3 变化通知 值变为 26.2 DRV-->>DEV: 确认收到 Note over DRV: 临近有效期时 DRV->>DEV: 4 续订 继续订阅 DEV-->>DRV: 确认 续订成功
COV vs 轮询对比
对比维度COV 订阅定时轮询
实时性高——变化即推送取决于轮询间隔
网络开销低——仅变化时发送高——持续占用带宽
可靠性需处理订阅过期/丢失简单可靠
适用场景频繁变化的关键点位所有场景(兜底方案)

本驱动策略:COV + 轮询双模式——优先 COV,失败自动回退轮询。

6

写入优先级——谁的命令优先

为什么需要优先级

同一个设备可能同时被多个系统控制:
· 消防系统:检测到火灾,紧急关闭新风 → 最高优先
· 定时策略:下班时间自动关空调 → 中等优先
· 运维人员:手动关闭某台设备 → 一般优先

如果同时下达不同指令,设备该听谁的?BACnet 用 16 级优先级解决:编号越小优先级越高。

16 级优先级详解
graph LR P1["1 手动生命安全"] --> P2["2 自动生命安全"] P2 --> P3["3 手动紧急"] P3 --> P4["4-7 制冷加热等"] P4 --> P5["8 手动操作 默认"] P5 --> P6["9 自动操作"] P6 --> P7["10-16 现场自定义"] style P1 fill:#ff3b30,color:#fff style P5 fill:#007aff,color:#fff style P7 fill:#f2f2f7,color:#8e8e93
优先级工作原理示例
graph TB subgraph PA["空调机组 Priority Array"] S6["槽位 6 自动制冷: 开启"] S8["槽位 8 手动操作: 关闭"] S12["槽位 12 循环控制: 开启"] end RESULT["实际输出 = 槽位 6 = 开启
6 是非空槽位中编号最小的"] PA --> RESULT FIRE["消防触发
槽位 1 = 关闭"] FIRE --> RESULT2["实际输出 = 槽位 1 = 关闭
消防优先级最高"] style S6 fill:#34c759,color:#fff style S8 fill:#ff9500,color:#fff style S12 fill:#8e8e93,color:#fff style RESULT fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style FIRE fill:#ff3b30,color:#fff style RESULT2 fill:#ff3b30,color:#fff

本驱动默认使用优先级 8(Manual-Operator)——高于自动控制(9~16),低于紧急和消防(1~7)。

7

BACnet 协议特点总结

面向对象模型
设备 = 多个对象,对象 = 多个属性,结构统一清晰
设备自动发现
Who-Is/I-Am 广播自动找到所有设备,无需手动配置
COV 变化订阅
值变化时主动推送,实时性高、网络开销低
⚡
批量读取
一条请求读取多个属性,减少网络交互
优先级写入
16 级优先级,消防紧急自动覆盖常规控制
开放标准
打破厂商锁定,不同品牌设备自由组合
1

驱动整体架构

BACnet IP 驱动整体架构
graph TB subgraph PLAT["OptiCloud 平台"] PC["PlatformClient API"] DB[("数据存储")] end subgraph DRV["BacnetIpAccess"] INIT["初始化"] CB1["创建实例"] CB2["打开实例"] CB3["关闭实例"] CB4["更新实例"] CB5["删除实例"] CMD["命令下发"] end subgraph MGR["实例管理 BacnetInstanceManager"] H["InstanceHolder"] LD["LocalDevice BACnet4J"] RT["读线程"] CV["COV 管理"] end subgraph API["Web API"] SA["扫描设备接口"] BA["批量创建接口"] end subgraph DEV["设备 A/B/C"] D1["Device A"] D2["Device B"] D3["Device C"] end PLAT --> DRV CB2 --> H H --> LD & RT & CV LD -.->|"UDP 47808"| D1 LD -.->|"UDP 47808"| D2 LD -.->|"UDP 47808"| D3 SA -.->|"Who-Is/I-Am"| DEV BA --> PC RT --> LD CMD --> LD PC --> CB1 & CB2 & CB3 & CB4 & CB5 & CMD
项目目录结构
graph LR subgraph root["access/"] A["BacnetIpAccess.java"] IH["BacnetInstanceHolder.java"] IM["BacnetInstanceManager.java"] end subgraph core["core/"] C1["BacnetLocalDeviceFactory"] C2["BacnetConfigBuilder"] C3["BacnetDataTypeMapper"] C4["BacnetPropertyReader"] C5["BacnetPropertyWriter"] end subgraph disc["discovery/"] D1["BacnetDeviceDiscovery"] end subgraph ap["api/"] AP1["ScanDevicesApi"] AP2["BatchCreateInstancesApi"] AP3["ImportInstanceApi"] end subgraph tk["task/"] T1["BacnetAliveTask"] end A --> IH & IM & C1 & AP1 & AP2 & T1
设计定位:一个实例管理一台 BACnet 设备

本驱动采用 一实例一设备 模型——每个驱动实例对应一台 BACnet 物理设备。

优势
· 实例故障互不影响
· 独立线程互不阻塞
· 配置变更互不干扰
· 与平台模型完全对齐
扫描创建时自动实现
batchCreateInstances 自动为每台选中设备创建独立实例,每实例基于设备 ID 生成唯一 localDeviceId(100000 + deviceId),扫描/批量创建共用端口 47808 的共享 LocalDevice
2

实例属性设计

实例属性定义 (14 项)
属性标识显示名称类型必填默认值说明
host本机IP地址string必填0.0.0.0绑定所有网卡
port本机端口int必填47808BACnet UDP 端口
localDeviceId本机设备IDint必填自动生成网络内唯一
broadcastAddress广播地址string可选255.255.255.255Who-Is 广播
timeout通信超时(ms)int必填6000请求超时
retries重试次数int可选2失败重试
segmentTimeout分段超时(ms)int可选5000APDU 分段
segmentWindow分段窗口int可选5滑动窗口
maxReadMultipleReferences批量读取上限int可选20单次请求上限
deviceIp设备IP地址string可选-手动时用户填写,扫描时自动填充
devicePort设备端口int可选47808手动时用户填写,扫描时自动填充
deviceInstanceId设备实例号int可选-手动时用户填写,扫描时自动填充
syncInterval巡检周期int可选60对象变化巡检周期数(约5分钟)
maxCovSubscriptionsCOV订阅上限int可选50单实例最大COV订阅数,0=不限
实例属性分类
graph LR subgraph required["必填 — 通信配置"] A1["host"] A2["port"] A3["localDeviceId"] A4["timeout"] end subgraph advanced["可选 — 高级参数"] B1["broadcastAddress"] B2["retries"] B3["segmentTimeout"] B4["segmentWindow"] B5["maxReadMultipleReferences"] end subgraph target["可选 — 设备信息 (手动填写或自动填充)"] C1["deviceIp"] C2["devicePort"] C3["deviceInstanceId"] end subgraph runtime["可选 — 运行参数"] D1["syncInterval"] D2["maxCovSubscriptions"] end style required fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style advanced fill:#fff8e1,stroke:#ff9500,color:#1c1c1e style target fill:#e8f8f0,stroke:#34c759,color:#1c1c1e style runtime fill:#f5f0ff,stroke:#5856d6,color:#1c1c1e
3

点位属性设计

点位属性定义 (9 项)
propertyIdentifier 固定为 85(presentValue),每对象仅建一个点位
属性标识显示名称类型必填默认值
deviceIp设备IP地址string必填-
devicePort设备端口int必填47808
deviceInstanceId设备实例IDint必填-
objectType对象类型select必填-
objectInstance对象实例号int必填-
propertyIdentifier属性标识select必填85(固定 presentValue)
bacnetWriteTypeBACnet写入类型select可选自动探测
covSubscriptionCOV订阅select可选disabled
covIncrementCOV变化增量float可选0
数据类型自动探测(resolvePlatformDataType)

数据类型不再仅依赖对象类型推断,而是通过读取设备实际返回的 presentValue 探测真实类型。部分设备的 analogOutput 返回 SignedInteger 而非 Real,靠实际值探测可精确匹配。

BACnet Encodable 类型平台 dataTypebacnetWriteType
Real / DoublefloatReal / Double
UnsignedInteger / SignedIntegerintUnsignedInteger / SignedInteger
EnumeratedenumEnumerated
Boolean / BinaryPVint(返回 0/1)BinaryPV
CharacterStringstringCharacterString

BinaryPV 返回整数 0/1 而非布尔值,兼容平台 DevicePointListener 处理逻辑。

对象类型 → 访问模式映射
graph TD subgraph analog["模拟量 AI/AO/AV"] AD["dataType 由设备实际值探测
通常为 float"] end subgraph binary["二元量 BI/BO/BV"] BD["dataType = int
返回 0/1"] end subgraph multi["多态量 MSI/MSO/MSV"] MD["dataType = enum"] end style analog fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style binary fill:#ffe5e5,stroke:#ff3b30,color:#1c1c1e style multi fill:#fff8e1,stroke:#ff9500,color:#1c1c1e
点位唯一性约束
graph TD DI["deviceIp"] --> U["唯一性约束"] DP["devicePort"] --> U DII["deviceInstanceId"] --> U OT["objectType"] --> U OI["objectInstance"] --> U U --> R["同一实例下组合不重复
(propertyIdentifier 固定85不参与)"] style U fill:#007aff,color:#fff style R fill:#34c759,color:#fff
4

自动发现与扫描流程

scanDevices 扫描时序(快速/详细双模式)
sequenceDiagram participant FE as 前端 participant API as 扫描接口 participant LD as 共享LocalDevice(47808) participant DEV as BACnet 设备 FE->>API: 请求扫描(driverId, subnet, detailed) API->>LD: acquireSharedLocalDevice(引用计数+1) API->>API: 定向广播地址计算(192.168.1.x→.255) LD->>DEV: Who-Is 定向广播(可指定ID范围) DEV-->>LD: I-Am 响应 x N alt detailed=true 详细模式 par 并行读取(虚拟线程) LD->>DEV: ReadPropertyMultiple 厂商/型号/对象列表 DEV-->>LD: 设备完整信息 end else detailed=false 快速模式 LD->>LD: 仅从 I-Am 提取 IP/端口/设备ID(零远程读取) end API->>API: 排除已加入设备(查询平台实例) API->>LD: 缓存设备3分钟 + releaseSharedLocalDevice(计数-1) API-->>FE: 返回扫描结果
设备发现五大增强特性
快速扫描模式
detailed=false 仅取 IP/端口,零远程读取
排除已加入设备
查询平台实例,按 deviceInstanceId 去重
定向广播
subnet 自动算 .255 广播,减少全网流量
设备缓存(3min)
批量创建直接复用,无需重扫
共享LocalDevice
引用计数管理端口47808,避免冲突
设备发现协议交互
graph LR DRV["BACnet IP 驱动"] -->|"Who-Is 广播"| NET["BACnet 网络"] NET --> D1["设备A ID:101"] NET --> D2["设备B ID:102"] NET --> D3["设备C ID:103"] D1 -->|"I-Am"| DRV D2 -->|"I-Am"| DRV D3 -->|"I-Am"| DRV DRV -->|"ReadProperty"| D1 D1 -->|"厂商/型号/对象列表"| DRV
5

自动建模规则

扁平化建模:1 设备 = 1 DeviceType + 1 Device + N Point
flowchart TD START(["选中设备ID"]) --> CACHE["从扫描缓存取设备信息"] CACHE --> INST["创建驱动实例
localDeviceId=100000+deviceId"] INST --> OBJ["读取 Object List
过滤有效数据对象"] OBJ --> READ["批量读 objectName+presentValue
探测实际数据类型"] READ --> AMETA["预读模拟量元数据
units/min/max"] AMETA --> DT{"DeviceType
BACnet_id 存在?"} DT -->|No| NEW_DT["创建 DeviceType
命名: BACnet_{deviceInstanceId}
每对象一个 Property"] DT -->|Yes| REUSE_DT["复用 DeviceType"] NEW_DT --> DEV REUSE_DT --> DEV DEV["创建 1 个 Device
编码: deviceInstanceId
显示名: 设备描述"] DEV --> PTS["创建 N 个 Point
每对象1个Point(仅presentValue)
命名: objectName_{id}"] PTS --> FILL["填充 dataType/bacnetWriteType
+模拟量规格"] FILL --> BIND["自动绑点"] BIND --> DONE(["完成"]) style START fill:#007aff,color:#fff style DONE fill:#34c759,color:#fff style NEW_DT fill:#5856d6,color:#fff style DEV fill:#34c759,color:#fff style PTS fill:#ff9500,color:#fff
扁平化模型 ER(1 设备 = 1 DT + 1 Device + N Point)
erDiagram DeviceType ||--|| Device : 一台设备一个 Device ||--o{ Point : 每对象一个点位 DeviceType ||--o{ DeviceTypeProperties : 每对象一个属性 Point ||--o{ PointAttr : 拥有 DeviceType { string deviceTypeName PK "BACnet_{deviceInstanceId}" string deviceTypeKey UK string projectId string createSource } DeviceTypeProperties { string propertyIdentifier PK "objectType_objectInstance" string propertyName "objectName" string dataType "设备实际值探测" string propertyAccessMode "r / rw" } Device { string driverDeviceCode PK "deviceInstanceId" string displayName "设备描述" string deviceTypeKey FK string createByDriverInstanceId } Point { string pointName "objectName_{id}" string pointUniqueCode UK "typeKey&&id&&objType_objInst" string dataType "探测得出" string accessMode "r / rw" string curValue } PointAttr { string deviceIp string devicePort string deviceInstanceId string objectType string objectInstance string propertyIdentifier "固定 85 presentValue" string bacnetWriteType "探测类型" string covSubscription }
对象类型 → 访问模式
graph LR subgraph ro["只读 r"] AI2["AI analogInput"] BI2["BI binaryInput"] MSI2["MSI multiStateInput"] end subgraph rw["读写 rw"] AO2["AO analogOutput"] BO2["BO binaryOutput"] AV2["AV analogValue"] BV2["BV binaryValue"] MSO2["MSO multiStateOutput"] MSV2["MSV multiStateValue"] end style ro fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style rw fill:#fff8e1,stroke:#ff9500,color:#1c1c1e
点位命名规则
flowchart TD A["读取 objectName"] --> B{"objectName
非空?"} B -->|No| D["使用降级名
objectType_objectInstance"] B -->|Yes| C["objectName_{deviceInstanceId}"] D --> E["拼接 _{deviceInstanceId}"] style C fill:#34c759,color:#fff style E fill:#ff9500,color:#fff
6

自动绑点规则

绑定公式 — DEVICE_TYPE_KEY_DEVICE_CODE_PROPERTY_ID
flowchart LR A["deviceTypeKey"] --> RES["点位唯一码"] B[" && "] --> RES C["设备编码
deviceInstanceId"] --> RES D[" && "] --> RES E["属性标识
objectType_objectInstance"] E --> RES style RES fill:#007aff,color:#fff

示例:12345 && 101 && analogInput_1 注意:此处属性标识为 DeviceTypeProperty 的 objectType_objectInstance,而非 BACnet 属性编号(点位本身固定读 presentValue=85)。

自动绑点时序
sequenceDiagram participant DT as 设备类型 participant DEV as 设备 participant PT as 点位 participant PC as PlatformClient Note over DT: 步骤1 创建设备类型 DT->>PC: saveDeviceTypeBatch PC-->>DT: deviceTypeKey Note over DEV: 步骤2 创建设备(携带绑定规则) DEV->>PC: saveDeviceBatch rule PC-->>DEV: deviceId + devicePointList Note over PT: 步骤3 创建点位(按规则生成唯一码) PT->>PC: saveBatchPoint autoBind true PC->>PC: bindingPointForDriverInstance Note over PC: 绑定完成
7

数据采集架构

数据采集双模式架构
flowchart TB subgraph RT["读线程 per instance"] ST(["启动"]) --> RF["刷新点位缓存"] RF --> FT["分离轮询/COV点位"] FT --> LP{"遍历设备"} LP --> RD["ReadPropertyMultiple
批量读取"] RD --> PH["推送数值"] PH --> PT{"巡逻周期?"} PT -->|Yes| SC["轻量检测
仅读Object List"] SC --> CH{"有变化?"} CH -->|Yes| FS["执行增量同步"] CH -->|No| SL["休眠等待"] FS --> SL PT -->|No| SL SL --> LP end subgraph CV["COV 监听"] CS["COV 订阅"] --> DN["设备发送
COV通知"] DN --> HD["COV处理器"] HD --> PV["推送变化值"] end subgraph HB["心跳检测 每4秒"] HA["BacnetAliveTask"] --> DC["检测可达性"] DC --> DM{"状态变化?"} DM -->|"运行→离线"| MO["标记离线
暂停采集"] DM -->|"离线→运行"| MN["标记运行
恢复采集+重订阅COV"] DM -->|"无变化"| HA end style ST fill:#007aff,color:#fff style FS fill:#34c759,color:#fff style MO fill:#ff3b30,color:#fff style MN fill:#34c759,color:#fff
定时机制全景(5 种机制协同)

平台注册的定时任务只有 1 个(BacnetAliveTask,每 4 秒),其余均为线程内部循环或计数触发,减少平台调度负担。

flowchart TB subgraph PLAT["平台调度器 (每 4 秒)"] BAT["BacnetAliveTask
唯一注册任务"] end BAT --> S1["实例状态可达性检测"] BAT --> S2["读线程存活守护"] BAT --> S3["COV 续订 (顺带)"] S1 --> BO["指数退避
离线设备: 4s→8s→16s→32s→60s"] S2 --> RT subgraph INST["读线程 per instance (while 循环)"] RT["轮询读取
每 pollInterval (默认 5s)"] RT --> SYNC["定期巡检
每 ~5 分钟"] end style PLAT fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style INST fill:#e8f8f0,stroke:#34c759,color:#1c1c1e style BAT fill:#007aff,color:#fff style BO fill:#ff9500,color:#fff style RT fill:#34c759,color:#fff style SYNC fill:#5856d6,color:#fff
#机制周期实现方式
1BacnetAliveTask每 4 秒平台 ScheduleCallback
2读线程轮询每 5 秒Thread while 循环
3COV 续订心跳内顺带心跳任务内执行
4定期巡检每 ~5 分钟读线程计数器触发
5指数退避动态 4~60s心跳任务内时间判断
读线程主循环详细流程
flowchart TD START(["读线程启动"]) --> LOAD["加载点位按设备分组"] LOAD --> MAIN subgraph MAIN["主循环"] NR{"刷新
缓存?"} -->|Yes| RL["重新加载点位"] NR -->|No| SP RL --> SP["分离轮询/COV点位"] SP --> DL{"遍历
设备"} DL --> RD2["ReadPropertyMultiple"] RD2 --> OK{"读取成功?"} OK -->|Yes| CV2["值类型转换"] OK -->|No| ERR["记录错误"] CV2 --> COL["收集变更"] COL --> DL ERR --> DL DL -->|"完成"| HC{"有值
变更?"} HC -->|Yes| PB["pushPointValueBatch"] HC -->|No| PA PB --> PA{"巡逻
计数?"} PA -->|Yes| OL["读取Object List对比"] OL --> CHG{"检测到
变化?"} CHG -->|Yes| AS["自动增量同步
删/增/改"] CHG -->|No| WT AS --> WT["休眠等待"] PA -->|No| WT WT --> NR end style START fill:#007aff,color:#fff style AS fill:#34c759,color:#fff
COV 订阅生命周期
stateDiagram-v2 [*] --> Unsubscribed Unsubscribed --> Subscribed : 订阅COV Subscribed --> Receiving : COV通知 Receiving --> Subscribed : 等待下次通知 Subscribed --> RenewNeeded : 即将过期 RenewNeeded --> Subscribed : 续订COV Subscribed --> Unsubscribed : 取消订阅 Unsubscribed --> SubscribeFailed : 网络异常 SubscribeFailed --> Unsubscribed : 回退到轮询
COV 订阅数量上限与降级策略

单实例默认最多 50 个 COV 订阅(可通过 maxCovSubscriptions 配置,0=不限)。超出上限时,按优先级保留订阅,其余降级为轮询。

flowchart TD START(["COV 订阅请求"]) --> COUNT{"当前订阅数
< 上限?"} COUNT -->|"Yes"| SUB["正常订阅"] COUNT -->|"No"| PRI["按优先级排序"] PRI --> CMP["模拟量 > confirmed > unconfirmed"] CMP --> KEEP["高优先级: 保留COV订阅"] CMP --> DROP["低优先级: 降级为轮询"] DROP --> LOG["记录降级日志"] KEEP --> DONE(["完成"]) SUB --> DONE LOG --> DONE style START fill:#007aff,color:#fff style DONE fill:#34c759,color:#fff style DROP fill:#ff9500,color:#fff style KEEP fill:#34c759,color:#fff

优先级:analogInput/Output/Value (+10) > confirmed (+5) > unconfirmed (+3)

轮询容错:失败点位记忆 + 批量降级
flowchart TD POLL["逐设备轮询"] --> FILT["过滤 failedPointKeys 中点位"] FILT --> BATCH["ReadPropertyMultiple 批量读取"] BATCH --> BOK{"批次成功?"} BOK -->|No| FALL["降级为逐个 ReadProperty"] BOK -->|Yes| VAL["解析值"] FALL --> ONE{"单点成功?"} ONE -->|Yes 解码错误| RT["不加入跳过集合
允许后续重试"] ONE -->|No 设备错误| MEM["加入 failedPointKeys
后续轮询跳过"] RT --> VAL MEM --> SKIP["本轮跳过"] VAL --> PUSH["推送"] PUSH --> CLR["syncObjectList 时
清空 failedPointKeys"] style POLL fill:#007aff,color:#fff style FALL fill:#ff9500,color:#fff style MEM fill:#ff3b30,color:#fff style CLR fill:#34c759,color:#fff

BACnet4J 解码 bug(非设备永久故障)与设备真实错误分类处理,避免误跳过可恢复点位。

值推送策略
flowchart TD VAL["获取点位值"] --> CMP{"与上次值比较"} CMP -->|"值相同"| SKIP["跳过推送"] CMP -->|"值变化"| QUE["加入批量队列"] QUE --> TMR{"定时器到期
或批次满?"} TMR -->|Yes| PUSH["pushPointValueBatch"] TMR -->|No| WAIT["继续累积"] style PUSH fill:#007aff,color:#fff
8

命令下发流程

命令下发时序
sequenceDiagram participant P as 平台 participant A as BacnetIpAccess participant H as InstanceHolder participant LD as LocalDevice participant D as BACnet 设备 P->>A: 下发点位值 A->>A: 按设备分组 loop 每个设备 A->>H: 查找Holder H->>LD: 获取远程设备 LD->>D: 设备发现 D-->>LD: RemoteDevice loop 每个点位 A->>A: 按 bacnetWriteType 转换BACnet值 Note right of A: float → Real/Double Note right of A: int → Unsigned/SignedInteger Note right of A: int(0/1) → BinaryPV Note right of A: enum → Enumerated A->>LD: WritePropertyRequest 优先级8 LD->>D: UDP写入请求 D-->>LD: 写入响应(ACK/Error) Note right of A: INVALID_DATA_TYPE 时自动重试备用类型 end end
BACnet 写入优先级 (默认使用优先级 8)
graph LR P1["1 手动紧急"] P2["2 自动紧急"] P3["3 手动安全"] P4["4 自动安全"] P5["5 手动制冷"] P6["6 自动制冷"] P7["7 手动加热"] P8["8 手动操作 (默认)"] P9["9-16 其他级别"] style P1 fill:#ef4444,color:#fff style P8 fill:#007aff,color:#fff style P9 fill:#f2f2f7,color:#8e8e93
写入确认与异常反馈机制

WriteProperty 是确认型服务,设备必须返回响应。驱动利用此特性实现写入结果反馈。

flowchart TD WRITE["WriteProperty 请求"] --> RESP{"设备响应"} RESP -->|"ACK 确认"| OK["写入成功"] RESP -->|"Error 错误"| ERR["解析错误码"] RESP -->|"超时无响应"| TMO["通信超时"] ERR --> E1["WRITE_ACCESS_DENIED
对象不可写"] ERR --> E2["UNKNOWN_OBJECT
对象不存在"] ERR --> E3["VALUE_OUT_OF_RANGE
值超出范围"] ERR --> E4["INCONSISTENT_VALUE
类型不匹配"] OK --> LOG_OK["记录日志"] TMO --> FB["反馈到平台"] E1 --> FB E2 --> FB E3 --> FB E4 --> FB style WRITE fill:#007aff,color:#fff style OK fill:#34c759,color:#fff style ERR fill:#ff3b30,color:#fff style TMO fill:#ff9500,color:#fff style FB fill:#5856d6,color:#fff
INVALID_DATA_TYPE 自动重试策略

写入时按 bacnetWriteType 匹配设备期望类型。若仍收到 INVALID_DATA_TYPE / INCONSISTENT_VALUE,自动两阶段重试:

flowchart TD W["按 bacnetWriteType 写入"] --> R{"收到
类型错误?"} R -->|No| OK["成功"] R -->|Yes| S1["策略1: 诊断读取设备当前值
用设备实际类型写入"] S1 --> OK2{"成功?"} OK2 -->|Yes| OK OK2 -->|No| S2["策略2: 依次尝试预定义备用类型
getAlternativeWriteTypes"] S2 --> OK3{"任一成功?"} OK3 -->|Yes| OK OK3 -->|No| FAIL["全部失败 反馈平台"] style W fill:#007aff,color:#fff style OK fill:#34c759,color:#fff style S1 fill:#5ac8fa,color:#fff style S2 fill:#ff9500,color:#fff style FAIL fill:#ff3b30,color:#fff

错误码统一转大写下划线比较,兼容 BACnet4J 不同版本的 toString 格式差异。

9

实例生命周期与状态机

实例状态机
stateDiagram-v2 [*] --> CLOSED : 创建实例 CLOSED --> OFFLINE : 启用实例 switchOn=true OFFLINE --> RUNNING : 设备可达 RUNNING --> OFFLINE : 设备不可达 RUNNING --> CLOSED : 禁用实例 switchOn=false OFFLINE --> CLOSED : 禁用实例 switchOn=false note right of CLOSED 状态值: 0 未绑定端口 不采集数据 end note note right of RUNNING 状态值: 1 UDP端口已绑定 轮询/COV正常 end note note left of OFFLINE 状态值: 3 UDP端口已绑定 设备不可达 end note
实例完整生命周期
flowchart TD subgraph P1["① 创建"] CR["用户创建实例"] --> CB1["newDriverInstanceHasSaved"] CB1 --> VLD["属性校验"] VLD --> CCI["缓存初始化 状态=CLOSED"] end subgraph P2["② 启动"] EN["用户启用 switchOn=true"] --> CB2["openDriverInstance"] CB2 --> CLD["创建LocalDevice
绑定UDP端口"] CLD --> IHD["初始化InstanceHolder"] IHD --> LPT["加载点位按设备分组"] LPT --> SCV["订阅COV"] SCV --> SRT["启动读线程"] SRT --> SRUN["状态 → RUNNING"] end subgraph P3["③ 运行"] RUN["正常运行"] --> HB["心跳检测 每4秒"] HB --> CHK{"设备可达?"} CHK -->|Yes| RUN CHK -->|No| SOFF["状态 → OFFLINE"] SOFF --> RCV["设备恢复可达"] RCV --> RCV2["重订阅COV
重启读线程"] RCV2 --> SRUN2["状态 → RUNNING"] SRUN2 --> RUN RUN --> UPD["用户修改属性"] UPD --> CB3["driverInstanceHasUpdated"] CB3 --> REB["重建连接"] end subgraph P4["④ 关闭"] DIS["用户禁用 switchOn=false"] --> CB4["closeDriverInstance"] CB4 --> UNC["取消所有COV订阅"] UNC --> STT["停止读线程"] STT --> CLL["关闭LocalDevice
释放UDP端口"] CLL --> CLR["清除所有缓存"] CLR --> SCL["状态 → CLOSED"] end subgraph P5["⑤ 删除"] DEL["用户删除实例"] --> CB5["driverInstanceHasDeleted"] CB5 --> PRG["彻底清理所有资源"] end CCI --> EN SRUN --> RUN SCL --> DEL style CR fill:#007aff,color:#fff style EN fill:#34c759,color:#fff style DIS fill:#ff9500,color:#fff style DEL fill:#ff3b30,color:#fff style SOFF fill:#ff9500,color:#fff
心跳检测与状态维护
flowchart TD HBS(["心跳任务 每4秒"]) --> GA["getDriverData"] GA --> FI["过滤bacnetip实例"] FI --> IL{"遍历
实例"} IL --> SW{"switchOn?"} SW -->|"false"| EC["确保已关闭"] SW -->|"true"| INI{"已初始化?"} INI -->|No| OPI["openDriverInstance"] INI -->|Yes| CRT{"读线程
存活?"} CRT -->|No| RST["重启读线程"] CRT -->|Yes| CDV["检测可达性
Who-Is"] RST --> CDV CDV --> ANY{"有设备
可达?"} ANY -->|Yes| SR2["状态=RUNNING"] ANY -->|No| SO2["状态=OFFLINE"] SR2 --> PRV{"状态
变化?"} SO2 --> PRV PRV -->|"运行→离线"| PO["推送离线状态"] PRV -->|"离线→运行"| RC2["恢复逻辑
重订阅COV
触发同步"] PRV -->|"无变化"| NX PO --> NX["下一实例"] RC2 --> NX NX --> IL IL -->|"完成"| PB2["pushInstanceKeepAlive"] EC --> IL style HBS fill:#007aff,color:#fff style RC2 fill:#34c759,color:#fff
断线重连 — 指数退避策略

设备离线后,检测间隔按 min(4 x 2n-1, 60) 秒递增,避免频繁探测浪费资源。

flowchart LR F1["失败1次
等待4秒"] --> F2["失败2次
等待8秒"] F2 --> F3["失败3次
等待16秒"] F3 --> F4["失败4次
等待32秒"] F4 --> F5["失败5+次
等待60秒"] F5 --> RCV{"设备
恢复?"} RCV -->|"Yes"| RST["重置退避
立即恢复采集"] RCV -->|"No"| F5 style F1 fill:#ff9500,color:#fff style F5 fill:#ff3b30,color:#fff style RST fill:#34c759,color:#fff
连续失败次数退避间隔累计等待
14 秒4 秒
28 秒12 秒
316 秒28 秒
432 秒60 秒
5+60 秒(封顶)每 60 秒一次
10

对象变化自动同步

自动同步触发时机 (5 个触发点)
flowchart LR T1["首次打开实例
全量同步"] --> DET["变化检测"] T2["批量创建后
全量同步"] --> DET T3["离线恢复后
增量同步"] --> DET T4["属性更新后
增量同步"] --> DET T5["定期巡逻
每~60个读取周期"] --> DET DET --> LGT["轻量检测仅读Object List"] LGT --> CHG{"有变化?"} CHG -->|Yes| FLS["完整增量同步"] CHG -->|No| SKP["跳过继续轮询"] style DET fill:#007aff,color:#fff style FLS fill:#34c759,color:#fff
变化检测算法
flowchart TD START(["开始检测"]) --> RO["读取设备Object List"] RO --> BM["构建设备对象映射
objType_objInst → BacnetObject"] BM --> BP["构建平台对象映射
objType_objInst → Point"] BP --> DF["计算差集"] DF --> ADD["新增对象
设备有 平台没有"] DF --> REM["删除对象
平台有 设备没有"] DF --> COM["共有对象
逐个比较属性"] COM --> CMP["比较描述/单位
最小/最大/分辨率"] CMP --> MOD["修改对象
属性有差异"] ADD --> RPT["DeviceChangeReport"] REM --> RPT MOD --> RPT RPT --> HC{"有变化?"} HC -->|Yes| AA["直接执行增量同步
无需用户确认"] HC -->|No| END(["结束"]) style START fill:#007aff,color:#fff style AA fill:#34c759,color:#fff style ADD fill:#34c759,color:#fff style REM fill:#ff3b30,color:#fff style MOD fill:#ff9500,color:#fff
增量同步执行顺序 (删 → 增 → 改)
flowchart TD START(["检测到变化"]) --> P1 subgraph P1["阶段1: 删除"] DPT["删除对象对应的Point
deletePoints"] RMP["移除DeviceType对应Property"] end P1 --> P2 subgraph P2["阶段2: 新增"] NDT["更新DeviceType BACnet_id
添加新对象Property"] NDV["Device保持1个(无需新增)"] NPT["为新对象创建Point
1对象=1Point(presentValue)
探测类型"] end P2 --> P3 subgraph P3["阶段3: 修改 detectModifiedObjects"] UPT["自动修正点位名称
objectName_{id}"] UDT["自动修正dataType
与设备实际值对齐"] UWT["自动修正bacnetWriteType"] UAN["刷新模拟量规格
units/min/max"] end P3 --> CLR["清空failedPointKeys
允许重新读取"] CLR --> REF["刷新点位缓存"] REF --> DONE(["完成"]) style START fill:#ff3b30,color:#fff style DONE fill:#34c759,color:#fff style P1 fill:#ffe5e5,stroke:#ff3b30,color:#1c1c1e style P2 fill:#e8f8f0,stroke:#34c759,color:#1c1c1e style P3 fill:#fff8e1,stroke:#ff9500,color:#1c1c1e
detectModifiedObjects 自动修正已有点位
flowchart TD DET["批量读 objectName+presentValue"] --> LOOP{"遍历
已有对象"} LOOP --> NM["点位名称变化?
→ 更新 objectName_id"] LOOP --> DT["dataType 不匹配?
→ 探测值更新"] LOOP --> WT["bacnetWriteType 变化?
→ 更新写入类型"] LOOP --> SP["模拟量规格变化?
→ 更新 units/min/max"] NM --> CHG["收集变更点位"] DT --> CHG WT --> CHG SP --> CHG CHG --> SAVE["updateBatchPoint"] style DET fill:#007aff,color:#fff style SAVE fill:#34c759,color:#fff style CHG fill:#ff9500,color:#fff

设备固件升级后对象实际类型可能变化,自动修正保证点位信息始终与设备行为一致。

11

Web API 设计

API 总览 (3 个接口)
graph TB DRV["BACnet IP 驱动"] DRV --> SA["GET scanDevices
扫描网络设备"] DRV --> BA["POST batchCreateInstances
批量创建实例"] DRV --> IA["POST importInstance
单台设备导入"] SA --> R1["返回设备列表+缓存3分钟
快速模式/排除已加入/定向广播"] BA --> R2["projectId+deviceIds
1设备=1DT+1Device+N Point
自动绑点+探测类型"] IA --> R3["发现单台设备
IP+实例号定向导入"] style DRV fill:#5856d6,color:#fff style SA fill:#007aff,color:#fff style BA fill:#34c759,color:#fff style IA fill:#5ac8fa,color:#fff
scanDevices 参数
参数类型必填说明
driverIdLong必填驱动 ID
subnetString可选子网地址 默认0.0.0.0(全网),自动算定向广播
timeoutint可选超时秒数 默认 100
detailedstring可选true详细(默认)/false快速模式仅IP端口
deviceInstanceRangeLowint可选ID 范围下限
deviceInstanceRangeHighint可选ID 范围上限
batchCreateInstances 参数(已简化)
参数类型必填说明
projectIdString必填项目 ID
deviceIdsString必填设备ID列表(逗号分隔) 如 101,102,103
subsystemIdLong可选子系统 ID

driverId/driverName 由平台自动检测,设备信息从扫描缓存(3分钟)读取,无需重传 devices JSON。

三种实例创建方式对比
flowchart LR subgraph M["方式一:手动创建"] M1["填写 deviceIp
devicePort
deviceInstanceId"] --> M2["创建实例"] M2 --> M3["启用实例"] M3 --> M4["自动连接设备
firstTimeSync
自动同步对象"] end subgraph S["方式二:扫描创建"] S1["调用 scanDevices 扫描
支持subnet子网过滤"] --> S2["选择设备"] S2 --> S3["调用 batchCreateInstances"] S3 --> S4["自动建模+绑点
自动同步对象"] end subgraph I["方式三:单台导入"] I1["填写 deviceIp
deviceInstanceId"] --> I2["调用 importInstance"] I2 --> I3["确认设备信息"] I3 --> I4["调用 batchCreateInstances
完成创建"] end style M4 fill:#34c759,color:#fff style S4 fill:#34c759,color:#fff style I4 fill:#5ac8fa,color:#fff
12

批量导入设计

CSV 导入格式
graph TD HDR["deviceIp, devicePort, deviceInstanceId,
objectType, objectInstance,
propertyIdentifier, pointName"] R1["192.168.1.100, 47808, 101,
analogInput, 1, presentValue, 房间温度"] R2["192.168.1.100, 47808, 101,
analogOutput, 1, presentValue, 风门位置"] R3["192.168.1.100, 47808, 101,
binaryInput, 1, presentValue, 风机状态"] HDR --> R1 HDR --> R2 HDR --> R3 style HDR fill:#007aff,color:#fff
批量导入完整流程
flowchart TD UP(["上传CSV文件"]) --> PR["解析CSV"] PR --> VL{"行级校验"} VL --> FC["IP/端口/类型
格式校验"] VL --> UC["唯一性校验
同实例不重复"] FC --> RS{"校验结果"} UC --> RS RS -->|"有错误"| ER["立即返回错误
标记行号+原因"] RS -->|"全部通过"| GRP["按设备分组"] GRP --> LOOP{"遍历每台设备"} LOOP --> INS["创建驱动实例
端口自动递增"] INS --> DT["创建/复用设备类型"] DT --> DEV["创建设备"] DEV --> PTS["批量创建点位"] PTS --> LOG{"成功?"} LOG -->|"Yes"| SUCC["记录成功"] LOG -->|"No"| FAIL["记录失败原因
继续下一台"] SUCC --> LOOP FAIL --> LOOP LOOP -->|"完成"| RPT["返回导入结果
成功N台 失败M台"] RPT --> OK(["完成"]) style UP fill:#007aff,color:#fff style OK fill:#34c759,color:#fff style ER fill:#ff3b30,color:#fff style FAIL fill:#ff9500,color:#fff style RPT fill:#5856d6,color:#fff
导入错误处理策略
错误场景处理方式
CSV 格式错误立即返回错误,提示行号和字段
单台设备创建失败跳过该设备,继续下一台(部分成功模式)
端口冲突自动分配可用端口(47808 + N)
平台 API 调用失败重试 1 次,仍失败则跳过
点位数量超限校验阶段拦截,不执行创建
13

DriverAccess SDK 集成

驱动注册机制 (Java SPI)

驱动通过 Java SPI 机制注册到平台,在 META-INF/services/com.sctech.opti.cloud.drivers.sdk.DriverAccess 中声明实现类。平台启动时自动发现并加载驱动。

flowchart LR SPI["SPI 配置文件"] --> LOAD["平台扫描 ServiceLoader"] LOAD --> INST["实例化 BacnetIpAccess"] INST --> INIT["调用 init()"] INIT --> PC["注入 receivePlatformClient()"] PC --> REG["注册 ScheduleCallback
+ HttpApiCallBack"] REG --> READY["驱动就绪"] style SPI fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style READY fill:#34c759,color:#fff
DriverAccess 18 个方法实现映射
分类方法实现要点
基础
标识
init()轻量初始化(BACnet4J 日志、常量),重操作延迟到 openDriverInstance
getDriverName()返回 "bacnetip"
getDriverVersion()返回 "v 1.0.0"
getAccessMode()返回 EMBEDDED(内嵌模式,共享 JVM)
平台
通信
receivePlatformClient(pc)保存为静态字段,全局访问
getUniqueCodeByPointFromDriver()返回 null,使用平台默认绑定规则
扩展
点
getScheduleCallbackList()返回 [BacnetAliveTask],每 4 秒执行
getHttpApiCallBackList()返回 [ScanDevicesApi, BatchCreateInstancesApi]
校验validateDriverInstanceAttr()IP/端口/localDeviceId 唯一性校验
validateDriverInstanceAttrBatch()批量导入时的实例级校验
validateDriverPointAttr()对象类型/实例号/属性标识校验
validateDriverPointAttrBatch()批量导入时的点位级校验
实例
生命周期
newDriverInstanceHasSaved()缓存初始化,状态 CLOSED
openDriverInstance()创建 LocalDevice、绑定 UDP、启动读线程 + COV
closeDriverInstance()取消 COV、停止读线程、释放端口
driverInstanceHasUpdated()比较属性差异,必要时重建连接
driverInstanceHasDeleted()彻底清理所有资源
命令issuePointValueBatch()按设备分组、类型转换、WriteProperty 优先级 8
PlatformClient 核心方法使用场景
flowchart TB PC["PlatformClient
平台通信桥梁"] PC --> GDD["getDriverData()
心跳获取实例数据"] PC --> PPV["pushPointValueBatch()
推送采集值"] PC --> PIK["pushInstanceKeepAlive()
推送实例状态"] PC --> SDT["saveDeviceTypeBatch()
批量创建设备类型"] PC --> SDV["saveDeviceBatch()
批量创建设备"] PC --> SBP["saveBatchPoint()
批量创建点位"] PC --> BPF["bindingPointForDriverInstance()
自动绑点"] PC --> CDI["createDriverInstance()
CSV导入创建实例"] style PC fill:#007aff,color:#fff
14

异常处理与容错

异常处理全景(5 类场景)
flowchart TB ERR["异常源"] --> COMM["通信异常
超时/拒绝/畸形数据"] ERR --> PLAT["平台API异常
推送失败/超时"] ERR --> THRD["线程异常
读线程退出/虚拟线程拒绝"] ERR --> RES["资源异常
内存不足/端口占用"] ERR --> BAC["BACnet协议异常
Error/Reject/APDU过长"] COMM --> S1["重试 → 标记离线 → 指数退避"] PLAT --> S2["记录日志 → 丢弃批次 → 不重试"] THRD --> S3["心跳检测 → 自动重启线程"] RES --> S4["限制缓存大小 → 端口自动递增"] BAC --> S5["降级为轮询 → 跳过该点位"] style ERR fill:#ff3b30,color:#fff style S1 fill:#34c759,color:#fff style S2 fill:#ff9500,color:#fff style S3 fill:#34c759,color:#fff style S4 fill:#5856d6,color:#fff style S5 fill:#007aff,color:#fff
通信异常处理策略
场景策略
设备超时按 retries 重试,失败标记离线
连接被拒绝标记设备离线
APDU 过长自动切换分段传输
BACnet4J 解码异常(readANY)不加入永久跳过,降级逐个单点读取
ReadPropertyMultiple 批次失败自动降级为逐个 ReadProperty,解码/设备错误分类
反复读取失败的点位加入 failedPointKeys 跳过,巡检时清空允许重试
WriteProperty INVALID_DATA_TYPE诊断读取设备类型+备用类型自动重试
WriteProperty WRITE_ACCESS_DENIED捕获 ErrorAPDUException,中文错误反馈
COV 订阅失败降级为轮询模式
UDP 端口占用共享 LocalDevice(引用计数),占用时报错提示
平台 API 异常处理
场景策略
pushPointValueBatch 失败丢弃当前批次,下次重新采集
saveBatchPoint 失败返回错误到调用方
pushInstanceKeepAlive 失败WARN 日志,不影响采集
平台 API 超时跟随平台超时设置
并发与线程安全策略
共享资源并发场景安全策略
BacnetInstanceHolder 缓存心跳线程写 vs 读线程读ConcurrentHashMap 读写不冲突
点位值缓存读线程 + COV 线程并发写ConcurrentHashMap put 覆盖
LocalDevice读线程 + COV 回调 + 命令下发单实例单 LocalDevice,BACnet4J 内部同步
PlatformClient 调用多线程同时调用PlatformClient 本身线程安全

核心原则:每个实例独立线程和资源,实例之间完全隔离,一个实例的异常不影响其他实例。

15

性能调优参考

推荐配置参数
参数默认值推荐范围调优建议
pollInterval5000ms3000~10000过短浪费带宽,过长延迟高
maxReadMultiple2010~50取决于设备支持程度
syncInterval60 周期30~120约 5 分钟,不宜过小
maxCovSub5020~100取决于设备支持的订阅数
timeout6000ms3000~15000网络差时适当调大
retries21~3不宜过大,避免阻塞读线程
资源占用估算
维度单实例100 实例
线程数1 读线程 + 1 COV~200 虚拟线程
内存~2MB~200MB
UDP 端口1 个100 个(47808 起递增)
网络带宽取决于点位数线性增长
性能调优核心建议
COV 优先策略
对频繁变化的关键点位启用 COV,减少轮询压力
批量读取优化
maxReadMultipleReferences 调大可减少网络交互次数
巡检适度
syncInterval 不宜过小,Object List 读取有开销
虚拟线程
JDK 21+ 虚拟线程,每个实例独立线程不消耗平台线程